NavLink
,用以判斷 to={"xx/xx"}
是否匹配當前路由NavLink
和 Link
的功能是一致的,區別在於可以判斷其to属性
是否是當前匹配到的路由。
NavLink
的 style 或 className 可以接收一個函式,函式接收一個含有 isActive
及 isPending
的物件為參數,可根據參數調整樣式。
import { ..., NavLink } from "react-router-dom";
<NavLink
to={`xx/xx`}
style={({isActive, isPending}) => {...}
className={({isActive, isPending}) => {...}
>
...
</NavLink>
// src/routes/root.js
import { NavLink, ... } from "react-router-dom";
...
// 新增樣式對應
const getNavLinkStyles = (status) => {
const { isActive, isPending } = status;
if (isActive) return "active";
else if (isPending) return "pending";
else return "";
};
...
<NavLink
to={`contacts/${contact.id}`}
className={(status) => getNavLinkStyles(status)}
>
...
</NavLink>
目前操作結果:https://codesandbox.io/s/react-router-tutorial-active-link-styling-gjuxw8
Components - NavLink
: https://reactrouter.com/en/main/components/nav-link
useNavigation()
取得路由導航的相關資訊import { useNavigation } from "react-router-dom";
function SomeComponent() {
const navigation = useNavigation();
// 目前路由導航的狀態
navigation.state;
// 路由導航完成後,下一頁的位置
navigation.location;
// 路由導航 action 對應 Form 的相關資訊
navigation.formData;
navigation.formAction;
navigation.formMethod;
}
// src/routes/root.jsx
import { ..., useNavigation } from "react-router-dom";
...
const navigation = useNavigation();
...
<div
id="detail"
className={
navigation.state === "loading" ? "loading" : ""
}
>
<Outlet />
</div>
/* src/index.css */
#detail.loading {
opacity: 0.25;
transition: opacity 200ms;
transition-delay: 200ms;
}
目前操作結果:https://codesandbox.io/s/react-router-tutorial-use-navigation-efyqpk
Hooks - useNavigation
: https://reactrouter.com/en/main/hooks/use-navigation
/actionName
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
>
<button type="submit">Delete</button>
</Form>
const router = createBrowserROuter([
{
path: "xxx/:id",
element: ...,
loader: ...
},
{
path: "xxx/:id/edit",
element: ...,
loader: ...,
action: ...
},
{
path: "xxx/:id/destroy",
action: ...
},
])
// src/routes/contact.jsx
...
// 加上 Delete Form Submit 事件對應
const onDeleteSubmit = (event) => {
if (!window.confirm("Please confirm you want to delete this record.")) {
event.preventDefault();
}
};
...
<Form method="post" action="destroy" onSubmit={onDeleteSubmit}>
<button type="submit">Delete</button>
</Form>
import { ..., deleteContact } from "./contacts";
...
const contactDeleteAction = async ({ params }) => {
await deleteContact(params.contactId);
return redirect("/");
};
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
...,
children: [
{...},
{...},
{
path: "contacts/:contactId/destroy",
action: contactDeleteAction
}
]
}
]);
const contactDeleteAction = async ({ params }) => {
// FIXME: 故意丟出錯誤,用以後續加上對應路由的 errorElement
throw new Error("oh dang!");
await deleteContact(params.contactId);
return redirect("/");
};
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
...,
children: [
{...},
{...},
{
path: "contacts/:contactId/destroy",
action: contactDeleteAction,
errorElement: <div>Oops! There was an error.</div>
}
]
}
]);
...
目前操作結果:https://codesandbox.io/s/react-router-tutorial-delete-4rrpkm
當在巢狀路由中,指定一個路由設定為 index:true
用以取代 { path: ''}
,那麼這個路由,就會是上層路由的預設渲染路由頁面。
const router = createBrowserRouter([
{
path: "/",
...
children: [
{ index: true, element: ... },
/* existing routes */
],
},
]);
前面的範例,在巢狀路由上還沒設定預設路由,所以整個頁面重新進入時,會出現下面的畫面,右邊的區塊空白一片。
程式碼放在 gist,供大家抓取。
// src/main.jsx
// import 預設路由頁面元件
import Index from "./routes/index";
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />, // 要加入的首頁元件
...
// 設定的巢狀路由
children: [
{
// 預設路由
index: true,
element: <Index />
},
...
]
}
]);
目前操作結果:https://codesandbox.io/s/react-router-tutorial-index-route-1nf66g
route - index
: https://reactrouter.com/en/main/route/route#index
React Router DOM 提供了 useNavigate
Hook,可以讓你以程式的方式做導航,它會返回一個函式用以做導航的操作。
import { ..., useNavigate } from "react-router-dom";
...
const navigate = useNavigate();
...
這個 navigate 函式,可以使用二種方式操作。
第一個參數是要導航的頁面路由網址
第二個參數是選擇性的,可以用以設置導航操作的各種方式
navigate('/xxxx');
navigate('/xxxx', { replace: true});
參數傳入一個數字,這個數字是歷史記錄的堆疊數字位置。舉例來說,navigate(-1)
就如同在瀏覽器按下回到上一頁的操作。
navigate(-1);
另一個轉導方式是可以用在 loader 及 action function 的
redirect
前面的範例,在編輯聯絡人的頁面上,我們還沒實作「Cancel」,接下來就來完成實作,用以按下「Cancel」後,回到前一頁。
// src/routes/edit.jsx
import { ..., useNavigate } from "react-router-dom";
...
const navigate = useNavigate();
...
<button
type="button"
onClick={() => { navigate(-1); }}
>
Cancel
</button>
目前操作結果:https://codesandbox.io/s/react-router-tutorial-usenavigate-7bpkgo
Hooks - index
: https://reactrouter.com/en/main/route/route#index
之前的路由控制,不是從網址列上輸入URL做變更,就是提交 Form 表單觸發 Action。會觸發 Action 的 Form 表單提交方式,通常是 POST。如果不指定 Form 的表單提交方式,就是預設用 GET。
// Form 沒有指定 method 就是使用 get
<Form action="xxx">
<button type="submit">xxx</button>
</Form>
<Form method="post" action="xxx">
<button type="submit">xxx</button>
</Form>
用 GET 的形式提交表單,會帶入表單欄位參數且改變 URL
<Form><input name="xxx" value=""/></Form> => url?xxx=value
<Form role="search">
<input
id="searchField"
name="searchField"
...
/>
...
/>
...
</Form>
但不會觸發 Action,你可以在 Loader Function 中取得 URL params。
const loader = ({request}) => {
const url = new URL(request.url);
const searchField = url.searchParams.get("searchField");
const data = getDataBy(searchField);
return data;
// src/routes/root.jsx
<Form id="search-form" role="search">
<input
id="q"
name="q"
...
/>
<div id="search-spinner" aria-hidden hidden={true} />
...
</Form>
// src/main.jsx
const rootLoader = async ({ request }) => {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts, q };
};
?q=xxxx
,而 Loader Function 內可以使用 url.searchParams.get("q")
得到參數來查詢聯絡人資料後,顯示聯絡人資料查詢結果在畫面上。目前操作結果:https://codesandbox.io/s/react-router-tutorial-url-search-params-fbnzty
// src/routes/root.jsx
import { useEffect } from "react";
export default function Root() {
const { contacts, q } = useLoaderData();
...
// 當 url 的 q 變動,欄位 q 也會跟著連動
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
...
<Form id="search-form" role="search">
<input
id="q"
name="q"
defaultValue={q}
...
/>
{/* existing code */}
</Form>
...
}
目前操作結果:https://codesandbox.io/s/react-router-tutorial-url-search-params-advance-9wwdgn
透過 useSubmit Hook 取得的 submit 函式,可以不需要再按下 Enter 做表單提交,而是可以把 submit 函式放在你要控制的事件當中,做表單提交。
import { ..., useSubmit } from "react-router-dom";
...
const submit = useSubmit();
...
<input
...
on事件={(event) => {
submit(event.currentTarget.form);
}}
/>
// src/routes/root.jsx
import { ..., useSubmit } from "react-router-dom";
...
const submit = useSubmit();
...
<input
id="q"
...
onChange={(event) => {
submit(event.currentTarget.form);
}}
/>
目前操作結果:https://codesandbox.io/s/react-router-tutorial-usesubmit-hvv1rr
Hooks - useSubmit
: https://reactrouter.com/en/main/hooks/use-submit
使用 navigation.state 去判斷路由導航時的狀態。(idle/submitting/loading)
而 navigation.location 則是路由導航完成後,下一頁的位置。如果navigation.state 為 loading 時,location 就會有值,反之,loaction 會變空值,這也可以做為判斷的依據。
const navigation = useNavigation();
...
// 如果正在搜尋
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"your-search-field"
);
useSubmit 可以讓我們用程式化的方式,提交 Form 表單。
函式的第一個參數(Required) - Form 表單的資料。
函式的第二個參數(Optional) - 提交 Form 表單的額外資訊。
const submit = useSubmit();
...
submit(null, {
action: "/logout",
method: "post",
});
// same as
<Form action="/logout" method="post" />;
const submit = useSubmit();
...
submit(your-form-data, {
replace: true or false
});
...
const navigation = useNavigation();
...
// 判斷是否正在搜尋
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"q"
);
...
<input
id="q"
className={searching ? "loading" : ""}
// existing code
/>
<div
id="search-spinner"
aria-hidden
hidden={!searching}
/>
{replace:...}
,所以搜尋過程中,不會把 URL 加入到瀏覧歷程中。<input
id="q"
// existing code
onChange={(event) => {
const isFirstSearch = q == null;
submit(event.currentTarget.form, {
replace: !isFirstSearch,
});
}}
/>
目前執行結果:https://codesandbox.io/s/react-router-tutorial-usenavigation-and-usesubmit-xiq0fq
Hooks - useNavigation
: https://reactrouter.com/en/main/hooks/use-navigation
Hooks - useSubmit
: https://reactrouter.com/en/main/hooks/use-submit
useFetcher
Hook 可以讓路由不需要導航轉導頁面就可以操作,如同使用 App 一樣,當程式是高度互動的時候,會經使用 useFetcher
。
import { useFetcher } from "react-router-dom";
function SomeComponent() {
const fetcher = useFetcher();
// build your UI with these properties
fetcher.state; // idle/submitting/loading
fetcher.formData; // Optimistic UI
// render a form that doesn't cause navigation
return <fetcher.Form />;
}
使用 <fetcher.Form>
替代 <Form>
,無論指定什麼表單提交方法,都不會造成導航轉導。
function SomeComponent() {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/some/route">
<input type="text" />
</fetcher.Form>
);
}
當使用 <fetcher.Form> 或是 fetcher.submit() 時,fetcher.formData 相關的表單資料都可以用以樂觀更新 UI (Optimistic UI) - 也就是先顯示預期它成功的效果,如果失敗了就會回到原本的狀態。
function TaskCheckbox({ task }) {
let fetcher = useFetcher();
let status =
fetcher.formData?.get("status");
let isComplete = status === "complete";
return (
<fetcher.Form method="post">
<button
type="submit"
name="status"
value={isComplete ? "incomplete" : "complete"}
>
{isComplete ? "Mark Incomplete" : "Mark Complete"}
</button>
</fetcher.Form>
);
}
Optimistic UI 是一種前端 UI 快速回應用戶互動的概念,在發送資料給伺服器前,先展示預期成功的效果,用戶不需要等待才能操作下一步,因為對於大部份的用戶操作,服務器都可以成功完成確認更新。
理解 Optimistic UI 後,我們可以把畫面設計成如下
- 前端預期成功的畫面
- 若伺服器最終執行失敗的畫面
fetcher.Form
,無論怎麼提交表單都不會改變導航。// src/routes/contact.jsx
import { ..., useFetcher } from "react-router-dom";
function Favorite({ contact }) {
const fetcher = useFetcher();
...
<fetcher.Form method="post">
...
</fetcher.Form>
}
// src/main.jsx
const contactUpdateFavoriteAction = async ({ request, params }) => {
const formData = await request.formData();
await updateContact(params.contactId, {
favorite: formData.get("favorite") === "true"
});
};
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
/* existing code */
children: [
{ index: true, element: <Index /> },
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
// 加上要操作更新聯絡人加入最愛的 action
action: contactUpdateFavoriteAction,
},
/* existing code */
],
},
]);
function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
if (fetcher.formData) {
favorite = fetcher.formData.get("favorite") === "true";
}
/* existing code */
}
目前執行結果:https://codesandbox.io/s/react-router-tutorial-usefetcher-optimisticui-unzdrl
Hooks - useFetcher
: https://reactrouter.com/en/main/hooks/use-fetcher
我們在設計路由結構時,儘量讓錯誤頁面顯示在當下操作錯誤的 Outlet,這樣用戶不會看到整個錯誤頁面而毫無頭緒,而是還能操作其他功能。
// src/main.jsx
const contactLoader = async ({ params }) => {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("", {
status: 404,
statusText: "Not Found"
});
}
return contact;
};
children: [
{
errorElement: <ErrorPage />,
children: [
/* existing code */
]
}
]
目前執行結果:https://codesandbox.io/s/react-router-tutorial-contact-not-found-vcxjdt
React Router DOM 官方提供的 Tutorial 雖然跟著做完大概要花上不久的時間,但 step-by-step 的學習,可以完整理解到 React Router DOM 操作的奧妙之處,日後也能做出更多延伸的應用,請試著跟著學習看看。
終於要來到鐵人賽的最後一天了,關於 React 的生態系與相關應用,還有很多沒介紹到的地方,下一篇除了最後的總結,也會儘量搜集一些學習資源供大家與自己日後做參考。
https://reactrouter.com/en/6.4.1/start/tutorial
https://limboy.me/posts/react-router-6/
https://ithelp.ithome.com.tw/articles/10188245
https://ithelp.ithome.com.tw/articles/10226056
https://ithelp.ithome.com.tw/articles/10226370
https://ithelp.ithome.com.tw/articles/10282773
https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/implementing-react-router-dom-bf986888f2ce
https://tehub.com/a/9Nro8iXEsX
https://www.jianshu.com/p/e3f377ac8399